﻿using System;
using System.CodeDom;
using System.CodeDom.Compiler;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using Flatland.GeneticProgramming.CoreTypes;
using Flatland.GeneticProgramming.SyntaxTree;
using Flatland.Utilities;
using Microsoft.CSharp;

/**
* 
* Copyright (c) 2012 Anders Høst Kjærgaard | ahkj@itu.dk
* 
* 
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
* associated documentation files (the "Software"), to deal in the Software without restriction, 
* including without limitation the rights to use, copy, modify, merge, publish, distribute, 
* sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is 
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all copies or substantial 
* portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT 
* NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/

namespace Flatland.GeneticProgramming.Compilation {
    /// <summary>
    /// Compile class.
    /// </summary>
    public class Compiler {
        public string OutputNamespace { get; set; }

        /// <summary>
        /// Constructor.
        /// </summary>
        /// <param name="outputNamespace"></param>
        public Compiler(string outputNamespace) {
            OutputNamespace = outputNamespace;
        }

        /// <summary>
        /// Compiles the Tree contained in the Report.
        /// If succesful, the GenoType instance contained in the Report is set to an activated instance.
        /// </summary>
        /// <param name="report"></param>
        /// <param name="file"></param>
        /// <param name="generateCodeFile"></param>
        public GenoType Compile(Report report, string fileName, bool generateCodeFile = false)
        {
            //string fileName = GP.OUTPUT_FILE_PREFIX + file;
            string filePath = string.Empty;
            if(generateCodeFile)
                Directory.CreateDirectory(OutputNamespace);

            CodeCompileUnit compileUnit = new CodeCompileUnit();
            CodeNamespace codeNamespace = new CodeNamespace(OutputNamespace.Replace('\\', '.'));
            compileUnit.Namespaces.Add(codeNamespace);
            codeNamespace.Imports.Add(new CodeNamespaceImport("System"));
            codeNamespace.Imports.Add(new CodeNamespaceImport(report.GenoType.GetType().Namespace));
            codeNamespace.Types.Add(CodeTypeDeclaration(fileName, report));

            //CSharpCodeProcider Provides access to instances of the C# code generator and code compiler.
            CSharpCodeProvider provider = new CSharpCodeProvider();
            CompilerParameters cp = new CompilerParameters() { GenerateExecutable = false, GenerateInMemory = true, TreatWarningsAsErrors = false };
            foreach (var referencedAssembly in AppDomain.CurrentDomain.GetAssemblies()) {
                //Ignore dynamic assemblies
                if (!referencedAssembly.IsDynamic && !string.IsNullOrEmpty(referencedAssembly.Location))
                    cp.ReferencedAssemblies.Add(referencedAssembly.Location);
            }

            if (generateCodeFile) {
                filePath = OutputNamespace + "\\" + fileName;
                TextWriter tw = new IndentedTextWriter(new StreamWriter(filePath + ".cs", false));
                provider.GenerateCodeFromCompileUnit(compileUnit, tw, new CodeGeneratorOptions());
                tw.Close();
            }

            //Compile
           CompilerResults cr = provider.CompileAssemblyFromDom(cp, compileUnit);
            if (cr.Errors.Count != 0) {
                Console.WriteLine("Compile error : ");
                foreach (var error in cr.Errors) {
                    Console.WriteLine(error);
                }
                return null;
            }
            //Set the report
            report.FilePath = filePath;
            return (GenoType)Activator.CreateInstance(cr.CompiledAssembly.GetTypes()[0]);
        }

        /// <summary>
        /// Builds the CSharp type definition from the GenoType type passed to GP.
        /// </summary>
        /// <param name="typeName">Name of type</param>
        /// <param name="Tree">The GenoType Tree</param>
        /// <returns>The declared genotype</returns>
        private static CodeTypeDeclaration CodeTypeDeclaration(string typeName, Report report)
        {
            string className = typeName.Replace('.', '_');
            className = "comp_" + Utils.GetTimeStamp() + "_" + className;
            CodeTypeDeclaration codeTypeDeclaration = new CodeTypeDeclaration(className);
            codeTypeDeclaration.Attributes = MemberAttributes.Public;
            codeTypeDeclaration.CustomAttributes.Add(new CodeAttributeDeclaration("System.Serializable"));
            
            // get the base type of the GenoType provided, e.g. MathExample, we subclass this
            codeTypeDeclaration.BaseTypes.Add(report.GenoType.GetType().Name);
            codeTypeDeclaration.Members.AddRange(CodeMemberMethod(report, className));
            return codeTypeDeclaration;
        }

        /// <summary>
        /// Parses an execute method and add code statements.
        /// <param name="report">Report following the genotype</param>
        /// </summary>
        private static CodeTypeMemberCollection CodeMemberMethod(Report report, string enclosingClassName) {
            CodeTypeMemberCollection ctmc = new CodeTypeMemberCollection();
            //Get the execute method. 
            CodeMemberMethod execute = new CodeMemberMethod();
            execute.ReturnType = new CodeTypeReference(report.PrimitiveSet.IsCommand ? typeof(void) : typeof(object));
            execute.Attributes = MemberAttributes.Public;
            execute.Name = "Execute";
            Statements(report.GenoType.Tree, execute.Statements);
            ctmc.Add(execute);

            bool parameterLessFound = false;
            //Constructors
            foreach (var ctr in report.GenoType.GetType().GetConstructors(BindingFlags.Public | BindingFlags.DeclaredOnly | BindingFlags.Instance)) {
                CodeConstructor cs = new CodeConstructor();
                cs.Attributes = MemberAttributes.Public;
                if (ctr.GetParameters().Count() == 0) parameterLessFound = true;
                foreach (var parameterInfo in ctr.GetParameters()) {
                    cs.BaseConstructorArgs.Add(new CodeVariableReferenceExpression(parameterInfo.Name));
                    cs.Parameters.Add(new CodeParameterDeclarationExpression(parameterInfo.ParameterType, parameterInfo.Name));
                }
                ctmc.Add(cs);
            }
            //We need a parameterless for our Clone method
            if(!parameterLessFound)
            {
                CodeConstructor parameterless = new CodeConstructor();
                parameterless.Attributes = MemberAttributes.Public;
                ctmc.Add(parameterless);
            }

            //Make clonable
            CodeMemberMethod clone = new CodeMemberMethod();
            clone.ReturnType = new CodeTypeReference(typeof(object));
            clone.Attributes = MemberAttributes.Override | MemberAttributes.Public;
            clone.Name = "Clone";
            clone.Statements.Add(new CodeMethodReturnStatement(new CodeObjectCreateExpression(enclosingClassName)));
            ctmc.Add(clone);
            return ctmc;
        }

        /// <summary>
        /// Generate the statements that express the terminals and functions of the Tree
        /// </summary>
        /// <param name="Tree"></param>
        /// <param name="stmts"> </param>
        /// <returns></returns>
        private static void Statements(Tree tree, CodeStatementCollection stmts) {
            // CodeStatementCollection mCodeStatements = new CodeStatementCollection();
            //A Function, insert it as code, recursivly get parameters or subsequent Functions
            if (tree.GetType() == typeof(Node)) {
                Node Node = (Node)tree;
                switch (Node.Function.SetType) {
                    case SetType.IF_THEN_ELSE:
                        Debug.Assert(Node.Function != null);
                        Debug.Assert(Node.Children.Count == 3);
                        CodeExpression condition = Expression(Node.Children[0]);
                        CodeStatementCollection then = new CodeStatementCollection();
                        Statements(Node.Children[1], then);
                        CodeStatement[] thenArr = new CodeStatement[then.Count];
                        then.CopyTo(thenArr, 0);

                        CodeStatementCollection _else = new CodeStatementCollection();
                        Statements(Node.Children[2], _else);
                        CodeStatement[] elseArr = new CodeStatement[_else.Count];
                        _else.CopyTo(elseArr, 0);

                        stmts.Add(new CodeConditionStatement(condition, thenArr, elseArr));
                        break;

                    case SetType.SEQUENCE:
                        foreach (Tree child in Node.Children) {
                            CodeStatementCollection stmtSeq = new CodeStatementCollection();
                            Statements(child, stmtSeq);
                            stmts.AddRange(stmtSeq);
                        }
                        break;


                    case SetType.FUNCTION:
                        Debug.Assert(Node.Function != null && Node.Function.MethodInfo != null);

                        MethodInfo method = Node.Function.MethodInfo;
                        if (method.GetParameters().Any()) {
                            var parameters = new List<CodeExpression>(method.GetParameters().Count());
                            foreach (Tree child in Node.Children) {
                                parameters.Add(Expression(child));
                            }
                            if (method.ReturnType != typeof(void))
                                stmts.Add(new CodeMethodReturnStatement(Expression(tree)));
                            else
                                stmts.Add(new CodeMethodInvokeExpression(new CodeThisReferenceExpression(), method.Name, parameters.ToArray()));
                        }
                        else {
                            stmts.Add(new CodeMethodInvokeExpression(new CodeThisReferenceExpression(), method.Name,
                                                                  new CodeExpression()));
                            foreach (Tree child in Node.Children) {
                                Statements(child, stmts);
                            }
                        }
                        break;
                }
            }
            else {
                Leaf leaf = (Leaf)tree;
                Terminal terminal = leaf.Terminal;
                switch (terminal.SetType) {
                    case SetType.ZERO_ARITY_FUNCTION:
                        stmts.Add(new CodeMethodInvokeExpression(new CodeThisReferenceExpression(), terminal.MethodInfo.Name, new CodeExpression[] { }));
                        break;
                    case SetType.VARIABLE:
                        stmts.Add(new CodeVariableReferenceExpression(terminal.VarName));
                        break;
                    case SetType.ARRAY_INDEX:
                        stmts.Add(new CodeArrayIndexerExpression(new CodeVariableReferenceExpression(terminal.VarName),new CodePrimitiveExpression(
                                                                                     terminal.Index)));
                        break;
                    default: {
                            Console.WriteLine("Default");
                            break;
                        }
                }
            }
        }

        /// <summary>
        /// Generate the expressions that express the terminals and functions of the Tree
        /// </summary>
        /// <param name="Tree"></param>
        /// <returns></returns>
        private static CodeExpression Expression(Tree tree) {
            // CodeStatementCollection mCodeStatements = new CodeStatementCollection();
            var expr = new CodeExpression();
            //A Function, insert it as code, recursivly get parameters or subsequent Functions
            if (tree.GetType() == typeof(Node))
            {
                Node Node = (Node) tree;
                switch (Node.Function.SetType) {

                    case SetType.FUNCTION:
                        Debug.Assert(Node.Function != null && Node.Function.MethodInfo != null);
                        MethodInfo method = Node.Function.MethodInfo;
                        //Call parameters recursivly
                        if (method.GetParameters().Any()) {
                            var parameters = new List<CodeExpression>(method.GetParameters().Count());
                            foreach (Tree child in Node.Children) {
                                parameters.Add(Expression(child));
                            }
                            expr = new CodeMethodInvokeExpression(new CodeThisReferenceExpression(), method.Name,
                                                                  parameters.ToArray());
                            break;
                        }
                        break;
                }
            }
            else {
                Leaf leaf = (Leaf)tree;
                Terminal terminal = leaf.Terminal;
                switch (terminal.SetType) {
                    case SetType.ZERO_ARITY_FUNCTION:
                        expr = new CodeMethodInvokeExpression(new CodeThisReferenceExpression(), terminal.MethodInfo.Name, new CodeExpression[] { });
                        break;
                    case SetType.VARIABLE:
                        expr = new CodeVariableReferenceExpression(terminal.VarName);
                        break;
                    case SetType.ARRAY_INDEX:
                        expr = new CodeArrayIndexerExpression(new CodeVariableReferenceExpression(terminal.VarName),new CodePrimitiveExpression(
                                                                                     terminal.Index));
                        break;
                    case SetType.CONSTANT:
                        expr = new CodePrimitiveExpression(terminal.Constant);
                        break;
                    default:
                        {
                            throw new Exception("Terminal leaf SetType was unresolved");
                        }
                }

            }
            return expr;
        }
    }
}
